﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ADDDF.Infrastructure.DomainBase;
using ADDDF.Infrastructure.RepositoryFramework;
using ADDDF.Infrastructure.ChangeTracking.ChangeRecords;

namespace ADDDF.Infrastructure.ChangeTracking
{
    /// <summary>
    /// This class is a container for changed entities. A change on an entity might affect another
    /// entity. This can become a chained process. All of the entity that are brought back from the
    /// repository will be placed in the ChangeContext. The ChangeContext serve these purposes:
    /// 1. It serves as a temporary container for all changed entities. When the change process
    ///     completes they they can be saved into the repository at once. If any part of the change
    ///     process fail, they can be discarded altogether. Without ChangeContext, one needs to use
    ///     a transaction across the whole change process, and later commit or rollback the transation.
    ///     Not all repository technologies support transaction.
    /// 2. The information it contains can be used to generate a report of changes made during a
    ///     long process. This is useful when many entities are affected during the process.
    /// </summary>
    public class ChangeContext : IUnitOfWork
    {
        #region Fields

        private ChangeStore _changeStore;
        private IEntity _changeRoot;
        private bool _hasError;
        private Dictionary<EntityKey, IRepository> _repos;

        #endregion

        #region Constructors

        /// <summary>
        /// Construct the ChangeContext with an entity that is the one causes the changes.
        /// Root entity does not get persisted when commit.
        /// </summary>
        /// <param name="changeRoot"></param>
        public ChangeContext(IEntity changeRoot)
        {
            _changeRoot = changeRoot;
            _changeStore = new ChangeStore();
            _hasError = false;
            _repos = new Dictionary<EntityKey, IUnitOfWorkRepository>(new EntityKeyEqualityComparer());
            
            // add root into context
            GetEntity(changeRoot);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Return ChangeRoot.
        /// </summary>
        public IEntity ChangeRoot
        {
            get { return _changeRoot; }
        }

        /// <summary>
        /// Whether there is an error in the change context.
        /// </summary>
        public bool HasError
        {
            get { return _hasError; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Check if an entity is already in the context. If so, return the
        /// entity in the context, otherwise, add the entity into the context
        /// and return it.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="entity"></param>
        /// <returns></returns>
        public TEntity GetEntity<TEntity>(TEntity entity) where TEntity: IEntity
        {
            EntityRecord er = _changeStore.Find(entity.GetType(), entity.Key);

            if (er == null)
            {
                er = new EntityRecord(entity.GetType(), entity.Key, entity);
                _changeStore.Add(er);
            }

            return (TEntity) er.Value;
        }

        /// <summary>
        /// Mark the entity as changed for the given rule.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="forRule"></param>
        public void AddChange(IEntity entity, IChangeRule forRule)
        {
            EntityRecord er = _changeStore.Find(entity.GetType(), entity.Key);

            if (er == null)
            {
                er = new EntityRecord(entity.GetType(), entity.Key, entity);
                _changeStore.Add(er);
            }

            RuleRecord rr = _changeStore.Find(forRule);

            if (rr == null)
            {
                rr = new RuleRecord(forRule);
                _changeStore.Add(rr);
            }

            if (!er.RuleRecords.Contains(rr))
                er.AddRuleRecord(rr);

            if (!rr.EntityRecords.Contains(er))
                rr.AddEntityRecord(er);

            er.IsChanged = true;
        }

        /// <summary>
        /// Mark the entity as errored for given exception.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="exception"></param>
        public void AddError(IEntity entity, Exception exception)
        {
            ExceptionChangeRule rule = new ExceptionChangeRule(entity, exception);

            AddChange(entity, rule);

            _hasError = true;
        }

        /// <summary>
        /// Return all change rules.
        /// </summary>
        /// <returns></returns>
        public IList<IChangeRule> GetChangeRules()
        {
            return _changeStore.FindAllRuleRecords().Select(x => x.Rule).ToList();
        }

        /// <summary>
        /// Find changed entities for given rule.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="forRule"></param>
        /// <returns></returns>
        public IList<TEntity> GetEntities<TEntity>(IChangeRule forRule) where TEntity : IEntity
        {
            RuleRecord rr = _changeStore.Find(forRule);

            return rr.EntityRecords.Select(x => (TEntity)x.Value).ToList();
        }

        /// <summary>
        /// Accepting a list of entities. Create a new list of entities taken from
        /// the input list and from the context. The rule is this: if a given entity
        /// is in the context, then the one from the context will be in the final
        /// list, otherwise, the entity from the input list is used in the final
        /// list.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="entities"></param>
        /// <returns></returns>
        public IList<TEntity> ReplaceEntities<TEntity>(IList<TEntity> entities) where TEntity : IEntity
        {
            List<TEntity> newList = new List<TEntity>();

            foreach (TEntity entity in entities)
            {
                TEntity found = GetEntityNoAdd<TEntity>(entity);
                if (found != null)
                    newList.Add(found);
                else
                    newList.Add(entity);
            }

            return newList;
        }

        private TEntity GetEntityNoAdd<TEntity>(TEntity entity) where TEntity : IEntity
        {
            EntityRecord er = _changeStore.Find(entity.GetType(), entity.Key);

            if (er == null)
                return default(TEntity);
            else 
                return (TEntity)er.Value;
        }

        #endregion

        #region IUnitOfWork Implementation
        
        public void RegisterAdded(IEntity entity, IRepository repository)
        {
            throw new InvalidOperationException("ChangeContext does not support the Add operation");
        }

        /// <summary>
        /// This method can be invoked directly or by using the ChangeContext as IUnitOfWork.
        /// It stores the repository, associates it with the given entity, so that the
        /// entity can be committed with the Commit method.
        /// It also create a "reconstitute" of the entity, and place it in the context to
        /// replace the instance already in the context, or add if it did not already exist.
        /// A "reconstitute" of an entity is a simulation of the entity in the state when it
        /// is just returned from a repository. The reason we need this is because the 
        /// entity in this state returns false from its HasBeenChanged method to indicate it 
        /// has not been altered. The change manager needs a stopping point, that is when
        /// there can be no more changes. By simulating this "entity changes has already been
        /// saved into the database" we provide such stopping point.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="repository"></param>
        public void RegisterChanged(IEntity entity, IRepository repository)
        {
            // do nothing if entity is root entity
            if (_changeRoot.GetType().Equals(entity.GetType())
                && ((_changeRoot.Key == null && entity.Key == null)
                        || (_changeRoot.Key != null && _changeRoot.Key.Equals(entity.Key))))
                return;

            // register repository with this entity
            EntityKey eKey = new EntityKey{Type = entity.GetType(), Key = entity.Key };

            if (!_repos.ContainsKey(eKey))
                _repos.Add(eKey, repository);
            
            // create reconsitute of the entity
            ICanReconsituteFromSelf canReconsitute = entity as ICanReconsituteFromSelf ;

            if (canReconsitute == null)
                throw new D3Exception("Entity does not implement ICanReconstituteFromSelf");

            IEntity justLikeNew = canReconsitute.ReconstituteFrom(entity);

            // replace context entity with reconstitute one
            EntityRecord entityRecord = new EntityRecord(entity.GetType(), entity.Key, justLikeNew);

            if (_changeStore.Find(entity.GetType(), entity.Key) == null)
                _changeStore.Add(entityRecord);
            else
                _changeStore.Change(entityRecord);
        }

        public void RegisterRemoved(IEntity entity, IRepository repository)
        {
            throw new InvalidOperationException("ChangeContext does not support the Remove operation");
        }

        public void Commit()
        {
            foreach (var kv in _repos)
            {
                EntityRecord entityRecord = _changeStore.Find(kv.Key.Type, kv.Key.Key);
                //kv.Value.PersistUpdatedItem(entityRecord.Value);
            }
                
        }

        #endregion                     
    }
}
